import datetime
import functools
import logging
import math

from tornado.ioloop import PeriodicCallback

from coin.base.param_util import to_list
from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway_config import convert_og_config_from_dict
from coin.exchange.base.strategy_logger import StrategyLogProvider
from coin.exchange.kr_rest.product.product_impl import (generate_product_from_str,
                                                        generate_product_from_str2,
                                                        generate_product_from_subreq)
from coin.strategy.executor.aggressive.util import (convert_executor_config_from_dict)
from coin.strategy.executor.passive.factory import construct_agg_executor
from coin.strategy.executor.util.cancel_all_executor import (SimpleCancelAllExecutor)
from coin.strategy.hk_lm.model_fast_profile.profiles import ModelFastProfile
from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.mm.base.unary_multifeed_strategy_base import UnaryMultifeedStrategyBase
from coin.strategy.mm.subscription import (FeedSubscriptionRequest,
                                           SubscriptionRequest,
                                           gen_feed_sub_request_with_prefer)
from coin.strategy.tool.fill_log_dumper import FillLogDumper
from coin.strategy.accounting.pnl_balance_printer.factory import get_pnl_balance_printer_cls
from coin.strategy.accounting.pnl_balance_printer.util import (get_fee_coin_product,
                                                               get_quote_currency,
                                                               reserve_as_proto)
from coin.strategy.hk_hard_arb.util import parse_feed_subreqs_config
from coin.strategy.util.request_util import convert_strategy_request_from_dict

from coin.strategy.hk_lm.lm_agg_logic import LmAggLogic
import coin.strategy.mm.fastfeature.feature_feed_converter as ffcv


def _check_strat_config(strat_config):
  assert 'common' in strat_config
  assert 'products' in strat_config
  assert 'trade_subreq' in strat_config['common']
  assert 'strategy_request' in strat_config['common']
  assert isinstance(strat_config['products'], dict)
  assert len(strat_config['products']) > 0


def ceil_price(price, tick_size):
  return math.ceil(price / tick_size) * tick_size


def floor_price(price, tick_size):
  return math.floor(price / tick_size) * tick_size


def _convert_api_version(subreq):
  if subreq.exchange in ('Huobi', 'Okex') and subreq.market_type == 'Futures':
    api_version = 'futures'
  else:
    api_version = subreq.api_version
  return api_version


class LmAggStrategyConfig(object):
  def __init__(self, config, start_time=None):
    _check_strat_config(config)
    self._config = config
    self._trade_subreq = SubscriptionRequest.from_dict(config['common']['trade_subreq'])
    self._start_time = start_time
    self._model_profile = self._gen_model_profile()
    if 'feed_subreqs' in config['common']:
      self._feed_subreqs = parse_feed_subreqs_config(config['common']['feed_subreqs'])
    else:
      self._feed_subreqs = None
    try:
      self._feed_subreq = FeedSubscriptionRequest.from_multi([
          value.converter_factory.feedspec.feed_sub_request
          for value in self._model_profile.values()
      ])
      self._feed_subreqs = list(self._feed_subreq._sub_request_to_products_map.keys())
      print("feed_spec overrides parse subreq")
    except Exception:
      print("failed to parse subreq from feed_spec")
    self._strategy_request = convert_strategy_request_from_dict(
        config['common']['strategy_request'])

  @property
  def feed_subreqs(self):
    return self._feed_subreqs

  @property
  def strategy_request(self):
    return self._strategy_request

  @property
  def trade_subreq(self):
    return self._trade_subreq

  @property
  def model_profile(self):
    return self._model_profile

  @property
  def og_params(self):
    og_params = self._config['common'].get('og_params', {})
    og_params = convert_og_config_from_dict(self._trade_subreq, og_params)
    return og_params

  def _gen_model_profile(self):
    profile_dict = {}
    common_config = self._config['common']
    for symbol, config in self._config['products'].items():
      config = {**common_config, **config}
      assert symbol not in profile_dict
      if self._start_time is not None:
        product = generate_product_from_subreq(self._trade_subreq,
                                               symbol,
                                               current_datetime=self._start_time)
        profile = ModelFastProfile(**config, datetime_override=self._start_time)
      else:
        product = generate_product_from_subreq(self._trade_subreq, symbol)
        profile = ModelFastProfile(**config)
      assert profile.product.symbol == product.symbol, \
             '%s vs %s' % (profile.product.symbol, product.symbol)
      profile_dict[symbol] = profile
    return profile_dict

  def get_products(self):
    return [profile.product for profile in self._model_profile.values()]

  def get_currencies(self):
    currencies = []
    for profile in self._model_profile.values():
      currencies.extend(profile.currency)
    return list(set(currencies))


class SingleProductSubStrategy(object):
  def __init__(self, *, model_profile, trade_product, dry_run, logger, strategy_logger):
    self._logger = logger
    self._strategy_logger = strategy_logger

    self._feed_products = tuple(model_profile.product_map.keys())
    self._feed_converter = model_profile.converter_factory.create_feed_converter(
        ffcv.FeatureFeedConverter)

    self._config = model_profile.execution_params
    self._config['leverage'] = 10
    self._execution_params = convert_executor_config_from_dict(trade_product.symbol, self._config)

    self._trade_subreq = model_profile.sub_key
    self._feed_subreqs = model_profile.feed_subreqs
    self._feed_subreq = model_profile.feed_subreq
    self._model = model_profile.model

    self._feed_converter.register_symbols(self._model)

    self._dry_run = dry_run
    self._book = BookMap([trade_product], logger=self._logger)
    self._logger.info(self._config)

    self._executor = None
    self._exit = False
    self._trade_product = trade_product

    self._lm_agg_logic = LmAggLogic(config=self._config,
                                    trade_product=trade_product,
                                    logger=self._logger)

  @property
  def execution_params(self):
    return self._execution_params

  @property
  def config(self):
    return self._config

  @property
  def book(self):
    return self._book

  def set_exit(self):
    self._exit = True

  def on_og_reset(self, *, order_gateway):
    self._logger.info('Intializing executor...')
    executor_params = convert_executor_config_from_dict(self._config['product'].symbol,
                                                        self._config)
    self._executor = construct_agg_executor(self._trade_subreq,
                                            executor_params,
                                            order_gateway,
                                            self._logger)
    self._strategy_logger.write_executor_config(executor_params)

  def prepare(self, feed_subsystem):
    assert feed_subsystem is not None
    prefer_subreqs = self._feed_subreqs or [None]

    prod_to_sub = {}
    for key, values in self._feed_subreq._sub_request_to_products_map.items():
      for value in values:
        prod_to_sub[value] = key

    for product in self._feed_products:
      assert product in prod_to_sub, (product, prod_to_sub)
      feed_sub_request = gen_feed_sub_request_with_prefer(product, prod_to_sub[product])
      feed_subsystem.subscribe_book(feed_sub_request, functools.partial(self.on_book, product))
      feed_subsystem.subscribe_trade(feed_sub_request, functools.partial(self.on_trade, product))

  def on_trade(self, product, trade):
    if self._feed_converter.update_trade(product, trade, self._model):
      self.try_fire_and_manage()

  def on_book(self, product, book):
    if self._exit:
      return

    if self._feed_converter.update_book(product, book, self._model):
      self._book.set_book(product, book)
      self.try_fire_and_manage()

  def try_fire_and_manage(self):
    self._lm_agg_logic.try_fire_and_manage(timestamp=get_timestamp(),
                                           executor=self._executor,
                                           feed_converter=self._feed_converter,
                                           model=self._model,
                                           book=self._book.get_book(self._trade_product),
                                           dry_run=self._dry_run)


@functools.lru_cache(maxsize=64)
def include_okex_futures_products(products):
  return any([
      (product.product_type == 'Futures' and product.exchange == 'Okex') for product in products
  ])


class LmAggStrategy(UnaryMultifeedStrategyBase):
  def __init__(self, strat_config, dry_run=True, logger=None):
    self._logger = logger or logging.getLogger('LmAggStrategy')
    assert isinstance(strat_config, LmAggStrategyConfig)
    self._config = strat_config

    products = self._config.get_products()
    currencies = self._config.get_currencies()
    reserve_map = self._gen_reserve_map()

    print_products = products
    self._additional_products = []

    trade_subreq = self._config.trade_subreq
    accounting_currencies = [get_quote_currency(product) for product in products]
    fee_coin_products = get_fee_coin_product(trade_subreq.market_type,
                                             trade_subreq.exchange,
                                             accounting_currencies)
    fee_coin_currencies = [product.base for product in fee_coin_products]
    products += fee_coin_products
    currencies += fee_coin_currencies
    for currency in fee_coin_currencies:
      reserve_map[currency.currency] = 0

    if len(products) > 0 and products[0].exchange == 'Quoinex':
      print_products = products.copy()
      bitflyer_btcjpy = generate_product_from_str('Bitflyer', None, 'BTC-JPY')
      print_products.append(bitflyer_btcjpy)
      self._additional_products.append(bitflyer_btcjpy)

    self._set_traits(self._config.trade_subreq)
    super().__init__(products=products, currencies=currencies, logger=self._logger)

    self._book = BookMap(print_products, logger=self._logger)
    self._print_products = print_products

    printer_cls = get_pnl_balance_printer_cls(self._config.trade_subreq)

    pos_bias_map = {}

    for product_str, prodconfig in self._config._config.get('products', {}).items():
      if 'pos_bias' in prodconfig:
        assert 'pos_bias_price' in prodconfig
        product = generate_product_from_str2(trade_subreq.market_type,
                                             trade_subreq.exchange,
                                             None,
                                             product_str)
        # tuple of pos_bias & pos_bias_price
        pos_bias_map[product] = (prodconfig['pos_bias'], prodconfig['pos_bias_price'])

    if len(pos_bias_map) > 0:
      print(printer_cls)
      self._printer = printer_cls(products,
                                  self._book,
                                  reserve_map,
                                  logger=self._logger,
                                  pos_bias_map=pos_bias_map)
    else:
      self._printer = printer_cls(products, self._book, reserve_map, logger=self._logger)
    self._fill_dumper = FillLogDumper(self._config.trade_subreq)

    self._order_canceled_after_og_ready = False
    self._exit_attempt = 0
    self._exit_executor = None

    self._strategy_logger = StrategyLogProvider(self._config.strategy_request)
    self._strategy_logger.write_request()
    if reserve_map:
      self._strategy_logger.write_reserve(reserve_as_proto(self._config.trade_subreq, reserve_map))

    self._single_product_sub_strategies = []
    for profile in self._config.model_profile.values():
      self._single_product_sub_strategies.append(
          SingleProductSubStrategy(model_profile=profile,
                                   trade_product=profile.product,
                                   dry_run=dry_run,
                                   logger=self._logger,
                                   strategy_logger=self._strategy_logger))

  @property
  def config(self):
    return self._config

  @property
  def strategy_logger(self):
    return self._strategy_logger

  def _gen_reserve_map(self):
    reserve_map = {}
    if self._config.trade_subreq.market_type == 'Spot':
      for profile in self._config.model_profile.values():
        currency = profile.product.base.currency
        reserve = 0.5 * (profile.execution_params['min_pos'] + profile.execution_params['max_pos'])
        reserve_map[currency] = reserve
    return reserve_map

  def dump_pnl_balance(self):
    self._printer.print_pnl_balance()
    pnl_balances = self._printer.calculate_pnl_balance()
    if pnl_balances is not None:
      for pnl_balance in pnl_balances:
        self._strategy_logger.write_pnl_balance(pnl_balance=pnl_balance, quote=None, subreq_id=None)

  def prepare(self):
    for product in self._products + self._additional_products:
      feed_sub_request = gen_feed_sub_request_with_prefer(product, None)
      self.feed_subsystem.subscribe_book(feed_sub_request, functools.partial(self.on_book, product))

    for strat in self._single_product_sub_strategies:
      strat.prepare(self.feed_subsystem)
    # Start dumping pnl balance every 30 seconds
    PeriodicCallback(self.dump_pnl_balance, 30 * 1000).start()

  def on_og_reset(self):
    if not self._order_canceled_after_og_ready:
      self.order_gateway.cancel_all()
      self._logger.info('Orders canceled after OGs are ready.')
      self._order_canceled_after_og_ready = True

    self._printer.register_order_gateway(self.order_gateway)
    for strat in self._single_product_sub_strategies:
      strat.on_og_reset(order_gateway=self.order_gateway)
    self.order_gateway.register_fill_callback(self._fill_dumper.dump_fill)
    self._exit_executor = SimpleCancelAllExecutor()

  def _on_price_limit_error(self, valid):
    need_close_only = not valid
    for strat in self._single_product_sub_strategies:
      if include_okex_futures_products(strat._feed_products):
        strat._executor.set_close_only(need_close_only)

  def on_book(self, product, book):
    self._book.set_book(product, book)

  def signal_handler(self, sig, frame):
    self._logger.warning('Caught signal: %s', sig)
    self._ioloop.add_callback_from_signal(self.on_exit)
    if hasattr(self.feed_subsystem, "stop"):
      print("try stopping feed subsystem")
      self.feed_subsystem.stop()

  def on_exit(self):
    for strat in self._single_product_sub_strategies:
      strat.set_exit()

    self._exit_attempt += 1
    if self._exit_attempt > 5:
      self._ioloop.add_callback(self._ioloop.stop)

    finished = True
    if self._exit_executor is not None:
      ogs = to_list(self.order_gateway)
      self._logger.info('Exit callback executed.')
      finished = self._exit_executor.cancel_all_on_exit(ogs)

    if finished:
      self._ioloop.add_callback(self._ioloop.stop)
    else:
      timeout = datetime.timedelta(seconds=2)
      self._ioloop.add_timeout(timeout, self.on_exit)
